跳到主要内容

Golang 之 函数式选项模式

先说场景,像 C#、TypeScript 这些语言都可以使用默认参数或者可选参数,如下:

可选参数:在参数名后面,冒号前面添加一个问号,则表明该参数是可选的。

function buildName(firstName: string, lastName?: string) { //lastName为可选参数
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
var result1 = buildName("Bob"); //正确调用 Bob
var result2 = buildName("Bob", "Adams"); //正确调用 Bob Adams

默认参数:在参数名后直接给定一个值,如果这个值没有被传入,那么将会被赋值为默认值。

function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}

var result1 = buildName("Bob"); //没有传入第二个参数,则被赋值为默认的smith,结果为:Bob Smith
var result2 = buildName("Bob", "Adams"); //结果为:Bob Adams

但是到了 Golang、Java 这些不支持可变参数的语言应该怎么实现这种效果呢?最终解决方案就是使用 函数式选项模式 这种设计方式

在介绍 函数式选项模式 之前,先介绍几样手法,并解释它们的缺点,慢慢改进,直到终极解决方案:函数式选项模式。

来看个例子。比方说我们有个叫 StuffClient 的服务,它有两个配置选项(超时和重试):

type StuffClient interface {
DoStuff() error
}

type stuffClient struct {
conn Connection
timeout int
retries int
}

这个结构体是私有的,所以我们要提供构造函数:

func NewStuffClient(conn Connection, timeout, retries int) StuffClient {
return &stuffClient{
conn: conn,
timeout: timeout,
retries: retries,
}
}

我们每次调用 NewStuffClient 时都得提供 timeout 和 retries。大多数时间我们就只想用默认参数,但是 Golang 不支持重载,所以无法定义 NewStuffClient 同名函数了,编译通不过的。

所以另一种办法是搞几个名字不同的构造函数:

func NewStuffClient(conn Connection) StuffClient {
return &stuffClient{
conn: conn,
timeout: DEFAULT_TIMEOUT,
retries: DEFAULT_RETRIES,
}
}

func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient {
return &stuffClient{
conn: conn,
timeout: timeout,
retries: retries,
}
}

这看起来也太蹩脚了。我们整个更好的,传个配置对象进去:

type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}
func NewStuffClient(conn Connection, options StuffClientOptions) StuffClient {
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}

现在即使不想指定任何选项也不得不创建一个配置对象并将其传递进来。而且也没有自动设置默认值,除非我们在代码中添加一堆检查,或者暴露一个 DefaultStuffClientOptions 变量来作为默认参数传递(有可能在哪里被修改的,可能会导致其他地方出问题)。

最好的解决方案是 Functional Options Pattern(函数式选项模式),利用 Go 的闭包特性。我们保留上面定义的 StuffClientOptions,还要再加点调料:

type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}

func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}

func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}

我们已经定义了 StuffClient 结构的可用选项。另外又定义了一个叫 StuffClientOption 的东西,以我们的结构体为参数。我们还定义了一堆 WithRetries 和 WithTimeout 方法,返回闭包。现在开始我们的表演:

var defaultStuffClientOptions = StuffClientOptions{
Retries: 3,
Timeout: 2,
}

func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}

现在我们定义了一个内部的变量包含了默认选项,再调整构造器来接受非定长参数。然后遍历 StuffClientOption,将返回的闭包应用至选项变量(回调这些闭包接受一个 StuffClientOptions 变量并改值)。

现在要做的就是使用它:

x := NewStuffClient(Connection{})
fmt.Println(x) // prints &{{} 2 3}


x = NewStuffClient(
Connection{},
WithRetries(1),
)
fmt.Println(x) // prints &{{} 2 1}


x = NewStuffClient(
Connection{},
WithRetries(1),
WithTimeout(1),
)
fmt.Println(x) // prints &{{} 1 1}

我们很容易就可以添加新的选项,只要改少量代码。

把所有片段拼起来后看上去是这样的:

var defaultStuffClientOptions = StuffClientOptions{
Retries: 3,
Timeout: 2,
}
type StuffClientOption func(*StuffClientOptions)

type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}

func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct {}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
func (c stuffClient) DoStuff() error {
return nil
}

还能更简洁点,干掉 StuffClientOptions 结构,直接在 StuffClient 应用选项。

var defaultStuffClient = stuffClient{
retries: 3,
timeout: 2,
}
type StuffClientOption func(*stuffClient)

func WithRetries(r int) StuffClientOption {
return func(o *stuffClient) {
o.retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *stuffClient) {
o.timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct{}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
client := defaultStuffClient
for _, o := range opts {
o(&client)
}

client.conn = conn
return client
}
func (c stuffClient) DoStuff() error {
return nil
}

References

Go语言设计模式之函数式选项模式 Golang 函数式选项模式